Merge "Revert "selenium: add new message banner test to user spec""
[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 ] );
348 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
349 }
350
351 if ( !is_null( $params['summary'] ) ||
352 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
353 ) {
354 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
355 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
356 }
357
358 if ( isset( $prop['langlinks'] ) ) {
359 if ( $skin ) {
360 $langlinks = $outputPage->getLanguageLinks();
361 } else {
362 $langlinks = $p_result->getLanguageLinks();
363 // The deprecated 'effectivelanglinks' option depredates OutputPage
364 // support via 'useskin'. If not already applied, then run just this
365 // one hook of OutputPage::addParserOutputMetadata here.
366 if ( $params['effectivelanglinks'] ) {
367 $linkFlags = [];
368 Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
369 }
370 }
371
372 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
373 }
374 if ( isset( $prop['categories'] ) ) {
375 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
376 }
377 if ( isset( $prop['categorieshtml'] ) ) {
378 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
379 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
380 }
381 if ( isset( $prop['links'] ) ) {
382 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
383 }
384 if ( isset( $prop['templates'] ) ) {
385 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
386 }
387 if ( isset( $prop['images'] ) ) {
388 $result_array['images'] = array_keys( $p_result->getImages() );
389 }
390 if ( isset( $prop['externallinks'] ) ) {
391 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
392 }
393 if ( isset( $prop['sections'] ) ) {
394 $result_array['sections'] = $p_result->getSections();
395 }
396 if ( isset( $prop['parsewarnings'] ) ) {
397 $result_array['parsewarnings'] = $p_result->getWarnings();
398 }
399
400 if ( isset( $prop['displaytitle'] ) ) {
401 $result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
402 $titleObj->getPrefixedText();
403 }
404
405 if ( isset( $prop['headitems'] ) ) {
406 if ( $skin ) {
407 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
408 } else {
409 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
410 }
411 }
412
413 if ( isset( $prop['headhtml'] ) ) {
414 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
415 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
416 }
417
418 if ( isset( $prop['modules'] ) ) {
419 if ( $skin ) {
420 $result_array['modules'] = $outputPage->getModules();
421 $result_array['modulescripts'] = $outputPage->getModuleScripts();
422 $result_array['modulestyles'] = $outputPage->getModuleStyles();
423 } else {
424 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
425 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
426 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
427 }
428 }
429
430 if ( isset( $prop['jsconfigvars'] ) ) {
431 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
432 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
433 }
434
435 if ( isset( $prop['encodedjsconfigvars'] ) ) {
436 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
437 $result_array['encodedjsconfigvars'] = FormatJson::encode(
438 $jsconfigvars,
439 false,
440 FormatJson::ALL_OK
441 );
442 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
443 }
444
445 if ( isset( $prop['modules'] ) &&
446 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
447 $this->addWarning( 'apiwarn-moduleswithoutvars' );
448 }
449
450 if ( isset( $prop['indicators'] ) ) {
451 if ( $skin ) {
452 $result_array['indicators'] = (array)$outputPage->getIndicators();
453 } else {
454 $result_array['indicators'] = (array)$p_result->getIndicators();
455 }
456 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
457 }
458
459 if ( isset( $prop['iwlinks'] ) ) {
460 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
461 }
462
463 if ( isset( $prop['wikitext'] ) ) {
464 $result_array['wikitext'] = $this->content->serialize( $format );
465 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
466 if ( !is_null( $this->pstContent ) ) {
467 $result_array['psttext'] = $this->pstContent->serialize( $format );
468 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
469 }
470 }
471 if ( isset( $prop['properties'] ) ) {
472 $result_array['properties'] = (array)$p_result->getProperties();
473 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
474 }
475
476 if ( isset( $prop['limitreportdata'] ) ) {
477 $result_array['limitreportdata'] =
478 $this->formatLimitReportData( $p_result->getLimitReportData() );
479 }
480 if ( isset( $prop['limitreporthtml'] ) ) {
481 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
482 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
483 }
484
485 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
486 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
487 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
488 }
489
490 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
491 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
492 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
493 $xml = $dom->saveXML();
494 } else {
495 $xml = $dom->__toString();
496 }
497 $result_array['parsetree'] = $xml;
498 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
499 }
500
501 $result_mapping = [
502 'redirects' => 'r',
503 'langlinks' => 'll',
504 'categories' => 'cl',
505 'links' => 'pl',
506 'templates' => 'tl',
507 'images' => 'img',
508 'externallinks' => 'el',
509 'iwlinks' => 'iw',
510 'sections' => 's',
511 'headitems' => 'hi',
512 'modules' => 'm',
513 'indicators' => 'ind',
514 'modulescripts' => 'm',
515 'modulestyles' => 'm',
516 'properties' => 'pp',
517 'limitreportdata' => 'lr',
518 'parsewarnings' => 'pw'
519 ];
520 $this->setIndexedTagNames( $result_array, $result_mapping );
521 $result->addValue( null, $this->getModuleName(), $result_array );
522 }
523
524 /**
525 * Constructs a ParserOptions object
526 *
527 * @param WikiPage $pageObj
528 * @param array $params
529 *
530 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
531 */
532 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
533 $popts = $pageObj->makeParserOptions( $this->getContext() );
534 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
535 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
536 $popts->setIsSectionPreview( $params['sectionpreview'] );
537 $popts->setEditSection( !$params['disableeditsection'] );
538 if ( $params['disabletidy'] ) {
539 $popts->setTidy( false );
540 }
541 $popts->setWrapOutputClass(
542 $params['wrapoutputclass'] === '' ? false : $params['wrapoutputclass']
543 );
544
545 $reset = null;
546 $suppressCache = false;
547 Hooks::run( 'ApiMakeParserOptions',
548 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
549
550 // Force cache suppression when $popts aren't cacheable.
551 $suppressCache = $suppressCache || !$popts->isSafeToCache();
552
553 return [ $popts, $reset, $suppressCache ];
554 }
555
556 /**
557 * @param WikiPage $page
558 * @param ParserOptions $popts
559 * @param bool $suppressCache
560 * @param int $pageId
561 * @param Revision|null $rev
562 * @param bool $getContent
563 * @return ParserOutput
564 */
565 private function getParsedContent(
566 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
567 ) {
568 $revId = $rev ? $rev->getId() : null;
569 $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT );
570
571 if ( $getContent || $this->section !== false || $isDeleted ) {
572 if ( $rev ) {
573 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
574 if ( !$this->content ) {
575 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
576 }
577 } else {
578 $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
579 if ( !$this->content ) {
580 $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] );
581 }
582 }
583 $this->contentIsDeleted = $isDeleted;
584 $this->contentIsSuppressed = $rev &&
585 $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED );
586 }
587
588 if ( $this->section !== false ) {
589 $this->content = $this->getSectionContent(
590 $this->content,
591 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
592 );
593 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
594 }
595
596 if ( $isDeleted ) {
597 // getParserOutput can't do revdeled revisions
598 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
599 } else {
600 // getParserOutput will save to Parser cache if able
601 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
602 }
603 if ( !$pout ) {
604 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
605 }
606
607 return $pout;
608 }
609
610 /**
611 * Extract the requested section from the given Content
612 *
613 * @param Content $content
614 * @param string|Message $what Identifies the content in error messages, e.g. page title.
615 * @return Content
616 */
617 private function getSectionContent( Content $content, $what ) {
618 // Not cached (save or load)
619 $section = $content->getSection( $this->section );
620 if ( $section === false ) {
621 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
622 }
623 if ( $section === null ) {
624 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
625 $section = false;
626 }
627
628 return $section;
629 }
630
631 /**
632 * This mimicks the behavior of EditPage in formatting a summary
633 *
634 * @param Title $title of the page being parsed
635 * @param Array $params the API parameters of the request
636 * @return Content|bool
637 */
638 private function formatSummary( $title, $params ) {
639 global $wgParser;
640 $summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
641 $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
642
643 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
644 if ( $sectionTitle !== '' ) {
645 $summary = $params['sectiontitle'];
646 }
647 if ( $summary !== '' ) {
648 $summary = wfMessage( 'newsectionsummary' )
649 ->rawParams( $wgParser->stripSectionName( $summary ) )
650 ->inContentLanguage()->text();
651 }
652 }
653 return Linker::formatComment( $summary, $title, $this->section === 'new' );
654 }
655
656 private function formatLangLinks( $links ) {
657 $result = [];
658 foreach ( $links as $link ) {
659 $entry = [];
660 $bits = explode( ':', $link, 2 );
661 $title = Title::newFromText( $link );
662
663 $entry['lang'] = $bits[0];
664 if ( $title ) {
665 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
666 // localised language name in 'uselang' language
667 $entry['langname'] = Language::fetchLanguageName(
668 $title->getInterwiki(),
669 $this->getLanguage()->getCode()
670 );
671
672 // native language name
673 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
674 }
675 ApiResult::setContentValue( $entry, 'title', $bits[1] );
676 $result[] = $entry;
677 }
678
679 return $result;
680 }
681
682 private function formatCategoryLinks( $links ) {
683 $result = [];
684
685 if ( !$links ) {
686 return $result;
687 }
688
689 // Fetch hiddencat property
690 $lb = new LinkBatch;
691 $lb->setArray( [ NS_CATEGORY => $links ] );
692 $db = $this->getDB();
693 $res = $db->select( [ 'page', 'page_props' ],
694 [ 'page_title', 'pp_propname' ],
695 $lb->constructSet( 'page', $db ),
696 __METHOD__,
697 [],
698 [ 'page_props' => [
699 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
700 ] ]
701 );
702 $hiddencats = [];
703 foreach ( $res as $row ) {
704 $hiddencats[$row->page_title] = isset( $row->pp_propname );
705 }
706
707 $linkCache = LinkCache::singleton();
708
709 foreach ( $links as $link => $sortkey ) {
710 $entry = [];
711 $entry['sortkey'] = $sortkey;
712 // array keys will cast numeric category names to ints, so cast back to string
713 ApiResult::setContentValue( $entry, 'category', (string)$link );
714 if ( !isset( $hiddencats[$link] ) ) {
715 $entry['missing'] = true;
716
717 // We already know the link doesn't exist in the database, so
718 // tell LinkCache that before calling $title->isKnown().
719 $title = Title::makeTitle( NS_CATEGORY, $link );
720 $linkCache->addBadLinkObj( $title );
721 if ( $title->isKnown() ) {
722 $entry['known'] = true;
723 }
724 } elseif ( $hiddencats[$link] ) {
725 $entry['hidden'] = true;
726 }
727 $result[] = $entry;
728 }
729
730 return $result;
731 }
732
733 private function formatLinks( $links ) {
734 $result = [];
735 foreach ( $links as $ns => $nslinks ) {
736 foreach ( $nslinks as $title => $id ) {
737 $entry = [];
738 $entry['ns'] = $ns;
739 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
740 $entry['exists'] = $id != 0;
741 $result[] = $entry;
742 }
743 }
744
745 return $result;
746 }
747
748 private function formatIWLinks( $iw ) {
749 $result = [];
750 foreach ( $iw as $prefix => $titles ) {
751 foreach ( array_keys( $titles ) as $title ) {
752 $entry = [];
753 $entry['prefix'] = $prefix;
754
755 $title = Title::newFromText( "{$prefix}:{$title}" );
756 if ( $title ) {
757 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
758 }
759
760 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
761 $result[] = $entry;
762 }
763 }
764
765 return $result;
766 }
767
768 private function formatHeadItems( $headItems ) {
769 $result = [];
770 foreach ( $headItems as $tag => $content ) {
771 $entry = [];
772 $entry['tag'] = $tag;
773 ApiResult::setContentValue( $entry, 'content', $content );
774 $result[] = $entry;
775 }
776
777 return $result;
778 }
779
780 private function formatLimitReportData( $limitReportData ) {
781 $result = [];
782
783 foreach ( $limitReportData as $name => $value ) {
784 $entry = [];
785 $entry['name'] = $name;
786 if ( !is_array( $value ) ) {
787 $value = [ $value ];
788 }
789 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
790 $entry = array_merge( $entry, $value );
791 $result[] = $entry;
792 }
793
794 return $result;
795 }
796
797 private function setIndexedTagNames( &$array, $mapping ) {
798 foreach ( $mapping as $key => $name ) {
799 if ( isset( $array[$key] ) ) {
800 ApiResult::setIndexedTagName( $array[$key], $name );
801 }
802 }
803 }
804
805 public function getAllowedParams() {
806 return [
807 'title' => null,
808 'text' => [
809 ApiBase::PARAM_TYPE => 'text',
810 ],
811 'revid' => [
812 ApiBase::PARAM_TYPE => 'integer',
813 ],
814 'summary' => null,
815 'page' => null,
816 'pageid' => [
817 ApiBase::PARAM_TYPE => 'integer',
818 ],
819 'redirects' => false,
820 'oldid' => [
821 ApiBase::PARAM_TYPE => 'integer',
822 ],
823 'prop' => [
824 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
825 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
826 'properties|parsewarnings',
827 ApiBase::PARAM_ISMULTI => true,
828 ApiBase::PARAM_TYPE => [
829 'text',
830 'langlinks',
831 'categories',
832 'categorieshtml',
833 'links',
834 'templates',
835 'images',
836 'externallinks',
837 'sections',
838 'revid',
839 'displaytitle',
840 'headhtml',
841 'modules',
842 'jsconfigvars',
843 'encodedjsconfigvars',
844 'indicators',
845 'iwlinks',
846 'wikitext',
847 'properties',
848 'limitreportdata',
849 'limitreporthtml',
850 'parsetree',
851 'parsewarnings',
852 'headitems',
853 ],
854 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
855 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
856 ],
857 ApiBase::PARAM_DEPRECATED_VALUES => [
858 'headitems' => 'apiwarn-deprecation-parse-headitems',
859 ],
860 ],
861 'wrapoutputclass' => 'mw-parser-output',
862 'pst' => false,
863 'onlypst' => false,
864 'effectivelanglinks' => [
865 ApiBase::PARAM_DFLT => false,
866 ApiBase::PARAM_DEPRECATED => true,
867 ],
868 'section' => null,
869 'sectiontitle' => [
870 ApiBase::PARAM_TYPE => 'string',
871 ],
872 'disablepp' => [
873 ApiBase::PARAM_DFLT => false,
874 ApiBase::PARAM_DEPRECATED => true,
875 ],
876 'disablelimitreport' => false,
877 'disableeditsection' => false,
878 'disabletidy' => false,
879 'generatexml' => [
880 ApiBase::PARAM_DFLT => false,
881 ApiBase::PARAM_HELP_MSG => [
882 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
883 ],
884 ApiBase::PARAM_DEPRECATED => true,
885 ],
886 'preview' => false,
887 'sectionpreview' => false,
888 'disabletoc' => false,
889 'useskin' => [
890 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
891 ],
892 'contentformat' => [
893 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
894 ],
895 'contentmodel' => [
896 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
897 ]
898 ];
899 }
900
901 protected function getExamplesMessages() {
902 return [
903 'action=parse&page=Project:Sandbox'
904 => 'apihelp-parse-example-page',
905 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
906 => 'apihelp-parse-example-text',
907 'action=parse&text={{PAGENAME}}&title=Test'
908 => 'apihelp-parse-example-texttitle',
909 'action=parse&summary=Some+[[link]]&prop='
910 => 'apihelp-parse-example-summary',
911 ];
912 }
913
914 public function getHelpUrls() {
915 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
916 }
917 }