Revert "selenium: add new message banner test to user spec"
[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 ( isset( $params['useskin'] ) ) {
292 $factory = MediaWikiServices::getInstance()->getSkinFactory();
293 $skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
294 } else {
295 $skin = null;
296 }
297
298 $outputPage = null;
299 if ( $skin || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ) {
300 // Enabling the skin via 'useskin', 'headhtml', or 'categorieshtml'
301 // gets OutputPage and Skin involved, which (among others) applies
302 // these hooks:
303 // - ParserOutputHooks
304 // - Hook: LanguageLinks
305 // - Hook: OutputPageParserOutput
306 // - Hook: OutputPageMakeCategoryLinks
307 $context = new DerivativeContext( $this->getContext() );
308 $context->setTitle( $titleObj );
309 $context->setWikiPage( $pageObj );
310
311 if ( $skin ) {
312 // Use the skin specified by 'useskin'
313 $context->setSkin( $skin );
314 // Context clones the skin, refetch to stay in sync. (T166022)
315 $skin = $context->getSkin();
316 } else {
317 // Make sure the context's skin refers to the context. Without this,
318 // $outputPage->getSkin()->getOutput() !== $outputPage which
319 // confuses some of the output.
320 $context->setSkin( $context->getSkin() );
321 }
322
323 $outputPage = new OutputPage( $context );
324 $outputPage->addParserOutputMetadata( $p_result );
325 $context->setOutput( $outputPage );
326
327 if ( $skin ) {
328 // Based on OutputPage::headElement()
329 $skin->setupSkinUserCss( $outputPage );
330 // Based on OutputPage::output()
331 foreach ( $skin->getDefaultModules() as $group ) {
332 $outputPage->addModules( $group );
333 }
334 }
335 }
336
337 if ( !is_null( $oldid ) ) {
338 $result_array['revid'] = intval( $oldid );
339 }
340
341 if ( $params['redirects'] && !is_null( $redirValues ) ) {
342 $result_array['redirects'] = $redirValues;
343 }
344
345 if ( isset( $prop['text'] ) ) {
346 $result_array['text'] = $p_result->getText( [
347 'allowTOC' => !$params['disabletoc'],
348 'enableSectionEditLinks' => !$params['disableeditsection'],
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 $popts->setWrapOutputClass(
544 $params['wrapoutputclass'] === '' ? false : $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 '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 }